import { hideScrollBar } from '../../theme/scrollbar.css';
import { drag } from '../../tools/drag';
import { DATA } from '../symbol';
import { ROLLER_OPTION_LINE_HEIGHT, rollerStyle } from './roller.css';
import '../../sound/sound';
import { Component, type StyledProps } from '@knno/jsx';
import { play } from '../../sound/player';
import { em2px, getFontZoom } from '../../tools/utils';

export type RollerProps = StyledProps<Roller> & {
	options?: (string | number)[];
	value?: string | number;
	onPick?: (this: Roller, value: string | number) => void;
};

export class Roller extends Component<RollerProps, Roller, HTMLDivElement> {
	private [DATA]: {
		pos: number;
		options: (string | number)[];
		lineHeight: number;
		calcTimer?: number;
		tmpPos: number;
		updatingScroll: boolean;
		animate: boolean;
	} = { pos: 0, options: [], lineHeight: 0, tmpPos: 0, updatingScroll: false, animate: true };

	private get optionDiv(): HTMLDivElement {
		return this.el.querySelector(':scope>div.options') as HTMLDivElement;
	}

	private get lineDiv(): HTMLDivElement {
		return this.el.querySelector(':scope>div.line') as HTMLDivElement;
	}

	constructor(props: RollerProps) {
		super(props);
		this.setOptions(props.options || []);
		if (props.value !== undefined) {
			this.setValue(props.value);
		}
	}

	protected render(): Node {
		let top: number;
		let moved = false;
		let pointerDown = false;
		const optionsDiv = (
			<div class={['options', hideScrollBar]} tabIndex={-1} onFocus={() => this.focus()} />
		) as HTMLDivElement;
		const root = (
			<div
				class={rollerStyle}
				style={this.props.style}
				tabIndex={0}
				onClick={(e) => {
					if (e.target && e.button === 0 && !moved) {
						// const l = this[DATA].lineHeight;
						let offsetTop: number;
						if (e.target === root) {
							offsetTop = 0;
						} else {
							offsetTop = (e.target as HTMLElement).offsetTop;
						}
						const p = optionsDiv.scrollTop + offsetTop + e.offsetY;
						const idx = Array.from(optionsDiv.children as HTMLCollectionOf<HTMLElement>).findIndex(
							(elem) => {
								return p >= elem.offsetTop && p <= elem.offsetTop + elem.offsetHeight;
							},
						);
						if (idx >= 0) {
							this[DATA].pos = idx;
							this.firePick();
							this.updateScroll();
						}
					}
				}}
				onKeyDown={(e) => {
					if (e.key === 'ArrowDown') {
						e.preventDefault();
						e.stopPropagation();
						this.moveDown();
					} else if (e.key === 'ArrowUp') {
						e.preventDefault();
						e.stopPropagation();
						this.moveUp();
					}
				}}
				onWheel={(e) => {
					e.preventDefault();
					e.stopPropagation();
					if (e.deltaY > 0) {
						this.moveDown();
					} else if (e.deltaY < 0) {
						this.moveUp();
					}
				}}
				onPointerUp={(evt) => {
					evt.stopPropagation();
					evt.preventDefault();
				}}
				onPointerDown={(evt) => {
					if (evt.button === 0) {
						pointerDown = true;
						moved = false;
						top = optionsDiv.scrollTop;
						drag(
							this.el,
							evt,
							(_, distance) => {
								moved = true;
								optionsDiv.scrollTop = top - distance.y;
								this.calculateValue(false);
							},
							(_, speed) => {
								pointerDown = false;
								if (moved) {
									let speedY = speed.y;
									const doMove = () => {
										if (pointerDown) return;
										if (Math.abs(speedY) > 0.1) {
											const distance = speedY * 16;
											const top = optionsDiv.scrollTop - distance;
											optionsDiv.scrollTop = top;
											this.calculateValue(false);
											speedY *= 0.95;
											requestAnimationFrame(doMove);
										} else {
											this.calculateValue(true);
										}
									};
									requestAnimationFrame(doMove);
								}
							},
						);
					}
				}}
				onTouchMoveCapture={(evt) => {
					evt.preventDefault();
					evt.stopPropagation();
				}}
			>
				<div class="line" />
				{optionsDiv}
				<div class="mask" />
			</div>
		) as HTMLDivElement;
		let isQueued = false;
		const resizeObserver = new ResizeObserver(() => {
			if (!isQueued) {
				isQueued = true;
				requestAnimationFrame(() => {
					this.resize();
					isQueued = false;
				});
			}
			// this.resize();
		});
		resizeObserver.observe(root);
		return root;
	}

	private firePick() {
		this.el.dispatchEvent(new Event('change'));
	}

	moveDown() {
		if (this[DATA].pos < this[DATA].options.length - 1) {
			this[DATA].pos++;
			this.firePick();
			this.updateScroll();
		}
	}

	moveUp() {
		if (this[DATA].pos > 0) {
			this[DATA].pos--;
			this.firePick();
			this.updateScroll();
		}
	}

	private lineHeightCache?: number;
	private resize() {
		const height = this.el.clientHeight;
		// console.log('client height:', height);
		const optDiv = this.optionDiv;
		let lineHeight: number;
		if (this.lineHeightCache) {
			lineHeight = this.lineHeightCache;
		} else {
			lineHeight = em2px(optDiv, ROLLER_OPTION_LINE_HEIGHT) / getFontZoom();
			this.lineHeightCache = lineHeight;
		}
		const margin = (height - lineHeight) / 2 + 'px';
		optDiv.style.paddingTop = margin;
		optDiv.style.paddingBottom = margin;
		const line = this.lineDiv;
		const top = (height + lineHeight) / 2 - lineHeight + 'px';
		line.style.top = top;
		this[DATA].lineHeight = lineHeight;
		this.setAnimate(false);
		this.updateScroll();
		this.setAnimate(true);
	}

	private updateScroll() {
		if (this[DATA].updatingScroll) return;
		this[DATA].updatingScroll = true;
		const optDiv = this.optionDiv;
		const l = this[DATA].lineHeight;
		if (!this[DATA].animate) {
			const targetPos = this[DATA].pos * l;
			requestAnimationFrame(() => {
				optDiv.scrollTo({ top: targetPos, behavior: 'instant' });
			});
			this[DATA].updatingScroll = false;
			return;
		}
		const doUpdate = () => {
			const targetPos = this[DATA].pos * l;
			const current = optDiv.scrollTop;
			const distance = targetPos - current;
			if (Math.abs(distance) > 1) {
				const direction = distance > 0 ? 1 : -1;
				// y = a * (x ** 2)  ==>  x = sqrt(y / a)
				const move = Math.sqrt(Math.abs(distance) / 0.9) * direction;
				if (Math.abs(distance) > 1) {
					optDiv.scrollTo({ top: current + move, behavior: 'instant' });
				} else {
					optDiv.scrollTo({ top: targetPos, behavior: 'instant' });
				}
				this.calculateValue(false, true);
				requestAnimationFrame(doUpdate);
			} else {
				this[DATA].updatingScroll = false;
			}
		};
		requestAnimationFrame(doUpdate);
	}

	private calculateValue(scroll: boolean, playOnly = false) {
		const optDiv = this.optionDiv;
		if (optDiv.offsetHeight <= 0) return;
		const pos = Math.round(optDiv.scrollTop / this[DATA].lineHeight);
		if (this[DATA].tmpPos !== pos) {
			this[DATA].tmpPos = pos;
			play('da');
		}
		if (!playOnly) {
			if (this[DATA].pos !== pos) {
				this[DATA].pos = pos;
				this.firePick();
			}
		}
		if (scroll) this.updateScroll();
	}

	get options(): (string | number)[] {
		return this[DATA].options;
	}
	setOptions(options: (string | number)[]): this {
		const oldValue = this.value;
		this[DATA].options = options;
		const optDiv = this.optionDiv;
		optDiv.replaceChildren(
			...options.map((s) => {
				return <div textContent={s + ''} />;
			}),
		);
		if (oldValue) {
			this.setValue(oldValue);
		} else {
			this[DATA].pos = 0;
		}
		this.setAnimate(false);
		this.updateScroll();
		this.setAnimate(true);
		return this;
	}

	get value(): string | number | null {
		return this[DATA].options[this[DATA].pos] ?? null;
	}
	setValue(value: string | number): this {
		const pos = this[DATA].options.indexOf(value);
		if (pos < 0) {
			if (this[DATA].pos >= this[DATA].options.length) {
				this[DATA].pos = this[DATA].options.length - 1;
			}
		} else {
			this[DATA].pos = pos;
		}
		this.setAnimate(false);
		this.updateScroll();
		this.setAnimate(true);
		return this;
	}

	get selected(): number {
		return this[DATA].options.length === 0 ? -1 : this[DATA].pos;
	}
	setSelected(value: number): this {
		this[DATA].pos = value;
		if (this[DATA].pos >= this[DATA].options.length) {
			this[DATA].pos = this[DATA].options.length - 1;
		} else if (this[DATA].pos < 0) {
			this[DATA].pos = 0;
		}
		this.updateScroll();
		return this;
	}

	setAnimate(value: boolean): this {
		this[DATA].animate = value;
		return this;
	}

	focus() {
		this.el.focus();
	}
}
